iOS 触摸事件机制 响应链

iOS的事件有好几种:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机上得按键来控制手机),其中最常用的就是Touch Events了,基本存在于每个app的每个地方,本文主要讲Touch Events。

响应链

在app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。

当事件触发时,如果第一响应者没有响应,那么会根据响应链,将事件传递给nextResponder,如果还是没响应, 传给下一个nextResponder,直到UIApplication。

Hit-TestingView

寻找响应事件的具体响应者,称为:Hit-Testing View。通过以下两个方法实现。

// 返回当前view中,响应事件的后代view,没有响应返回nil
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; 
// 判断当前view是否响应事件,通常就是判断point是否在self.bounds内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,这个view和view上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,UITouch头文件里面有一个view和gestureRecognizers的属性,就是hitTest view和它的手势。

hitTest:withEvent:工作流程示例代码

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 判断当前view能否响应事件
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    // 判断点击区域是否在当前view内
    if ([self pointInside:point withEvent:event]) {
        // 遍历子view,递归找出能响应的后代view
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        // 没有子view响应,返回self
        return self;
    }
    return nil;
}

应用

如何增大点击区域

重写 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect bounds = self.bounds;
    CGFloat widthDelta = MAX(0, 44.0 - bounds.size.width);
    CGFloat heightDelta = MAX(0, 44.0 - bounds.size.height);
    bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);    //注意这里是负数,扩大了之前的bounds的范围
    return CGRectContainsPoint(bounds, point);
}

参考资料